/*
 jChecs: a Java chess game sample 

 Copyright (C) 2006-2011 by David Cotton

 This program is free software; you can redistribute it and/or modify it under
 the terms of the GNU General Public License as published by the Free Software
 Foundation; either version 2 of the License, or (at your option) any later
 version.

 This program is distributed in the hope that it will be useful, but WITHOUT
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
 Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package fr.free.jchecs.core;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;

import static fr.free.jchecs.core.Constants.RANK_COUNT;
import static fr.free.jchecs.core.Piece.BLACK_KING;
import static fr.free.jchecs.core.Piece.BLACK_PAWN;
import static fr.free.jchecs.core.Piece.WHITE_KING;
import static fr.free.jchecs.core.Piece.WHITE_PAWN;
import static fr.free.jchecs.core.PieceType.KING;
import static fr.free.jchecs.core.PieceType.PAWN;
import static fr.free.jchecs.core.PieceType.QUEEN;

/**
 * Classes fournissant des fonctions utilitaires pour gérer la notation PGN.
 * <p>
 * Classe sûre vis-à-vis des threads.
 * </p>
 * 
 * @author David Cotton
 */
public final class SANUtils {
	/** Expression régulière permettant de valider une chaîne SAN. */
	// Découpage du pattern :
	// Mat/Pat/Nullité : (\\+{1,2}|#|\\(=\\))?
	// Petit roque : 0-0<Mat/Pat/Nullité>
	// Grand roque : 0-0-0<Mat/Pat/Nullité>
	// Pion sans prise : [a-h]([1-8]|[18][BKNQR])<Mat/Pat/Nullité>
	// Pion avec prise :
	// [a-h]x[a-h]((([1-8]|[18][BKNQR])<Mat/Pat/Nullité>)|([36]<Mat/Pat/Nullité>
	// e\\.p\\.))
	// Pièces (sauf pion) : [BKNQR][a-h]?[1-8]?x?[a-h][1-8]<Mat/Pat/Nullité>
	public static final Pattern SAN_VALIDATOR = Pattern
			.compile("^(0-0(\\+{1,2}|#|\\(=\\))?)|(0-0-0(\\+{1,2}|#|\\(=\\))?)|"
					+ "([a-h]([1-8]|[18][BKNQR])(\\+{1,2}|#|\\(=\\))?)|"
					+ "([a-h]x[a-h]((([1-8]|[18][BKNQR])(\\+{1,2}|#|\\(=\\))?)|"
					+ "([36](\\+{1,2}|#|\\(=\\))? e\\.p\\.)))|"
					+ "([BKNQR][a-h]?[1-8]?x?[a-h][1-8](\\+{1,2}|#|\\(=\\))?)$");

	/**
	 * Classe utilitaire : ne pas instancier.
	 */
	private SANUtils() {
		// Rien de spécifique...
	}

	/**
	 * Renvoi le mouvement correspondant à une chaine SAN appliquée à un état
	 * d'échiquier.
	 * 
	 * @param pEtat
	 *            Etat de l'échiquier.
	 * @param pSAN
	 *            Chaine SAN.
	 * @return Mouvement correspondant.
	 * @throws SANException
	 *             en cas d'erreur dans le format de la chaine SAN.
	 */
	public static Move toMove(final MoveGenerator pEtat, final String pSAN)
			throws SANException {
		if (pEtat == null) {
			throw new NullPointerException("Missing game state");
		}
		if (pSAN == null) {
			throw new NullPointerException("Missing SAN string");
		}

		if (!SAN_VALIDATOR.matcher(pSAN).matches()) {
			throw new SANException("Invalid SAN string [" + pSAN + ']', null);
		}

		final boolean trait = pEtat.isWhiteActive();
		if ("0-0".equals(pSAN)) {
			// Gère les petits roques...
			if (trait) {
				return new Move(WHITE_KING, Square.valueOf(4),
						Square.valueOf(6));
			}
			return new Move(BLACK_KING, Square.valueOf(60), Square.valueOf(62));
		} else if ("0-0-0".equals(pSAN)) {
			// ... les grands roques...
			if (trait) {
				return new Move(WHITE_KING, Square.valueOf(4),
						Square.valueOf(2));
			}
			return new Move(BLACK_KING, Square.valueOf(60), Square.valueOf(58));
		}

		// Gère les coups normaux...
		final Piece piece;
		int posSrc = 0;
		char c = pSAN.charAt(posSrc);
		if (Character.isLowerCase(c)) {
			if (trait) {
				piece = WHITE_PAWN;
			} else {
				piece = BLACK_PAWN;
			}
		} else {
			if (trait) {
				piece = Piece.valueOf(c);
			} else {
				piece = Piece.valueOf(Character.toLowerCase(c));
			}
			posSrc++;
		}

		final boolean prise = pSAN.indexOf('x') >= 0;
		final List<Move> mvts = new ArrayList<Move>(Arrays.asList(pEtat
				.getValidMoves(trait)));
		for (int i = mvts.size() - 1; i >= 0; i--) {
			final Move m = mvts.get(i);
			final boolean capture = m.getCaptured() != null;
			if ((piece != m.getPiece()) || (prise != capture)) {
				mvts.remove(i);
			}
		}

		int posDst = pSAN.length() - 1;
		while ((posDst > 0) && (!Character.isDigit(pSAN.charAt(posDst)))) {
			posDst--;
		}
		final Square dst = Square.valueOf(pSAN
				.substring(posDst - 1, posDst + 1));
		for (int i = mvts.size() - 1; i >= 0; i--) {
			final Move m = mvts.get(i);
			if (dst != m.getTo()) {
				mvts.remove(i);
			}
		}

		if (mvts.size() == 1) {
			return mvts.get(0);
		}

		if ((mvts.size() > 1)
				&& (((piece == BLACK_PAWN) && (dst.getRank() == 0)) || ((piece == WHITE_PAWN) && (dst
						.getRank() == 7)))) {
			// Supprime les ambiguités dues aux promotions...
			posDst = pSAN.length() - 1;
			while ((posDst > 0) && ("BNQR".indexOf(pSAN.charAt(posDst)) < 0)) {
				posDst--;
			}
			if (posDst > 0) {
				c = pSAN.charAt(posDst);
				for (int i = mvts.size() - 1; i >= 0; i--) {
					final Piece prom = mvts.get(i).getPromotion();
					if ((prom == null)
							|| (prom.getType().getSANLetter().charAt(0) != c)) {
						mvts.remove(i);
					}
				}
			} else {
				for (int i = mvts.size() - 1; i >= 0; i--) {
					if (mvts.get(i).getPromotion() != null) {
						mvts.remove(i);
					}
				}
			}
		}

		// Supprime les éventuelles ambiguités restantes...
		if (mvts.size() > 1) {
			c = pSAN.charAt(posSrc);
			if (Character.isLowerCase(c)) {
				final int col = c - 'a';
				for (int i = mvts.size() - 1; i >= 0; i--) {
					final Move m = mvts.get(i);
					if (col != m.getFrom().getFile()) {
						mvts.remove(i);
					}
				}
				posSrc++;
			}
		}

		if (mvts.size() > 1) {
			c = pSAN.charAt(posSrc);
			if (Character.isDigit(c)) {
				final int lig = c - '1';
				for (int i = mvts.size() - 1; i >= 0; i--) {
					final Move m = mvts.get(i);
					if (lig != m.getFrom().getRank()) {
						mvts.remove(i);
					}
				}
				posSrc++;
			}
		}

		final int l = mvts.size();
		if (l > 1) {
			throw new SANException("Ambiguous SAN string [" + pSAN + ']', null);
		} else if (l < 1) {
			throw new SANException("Illegal SAN string context [" + pSAN + ']',
					null);
		}

		return mvts.get(0);
	}

	/**
	 * Renvoi la chaine SAN correspondant à un mouvement pour un état
	 * d'échiquier.
	 * 
	 * @param pEtat
	 *            Etat de l'échiquier.
	 * @param pMouvement
	 *            Mouvement à traduire.
	 * @return Chaine SAN correspondante.
	 */
	public static String toSAN(final MoveGenerator pEtat, final Move pMouvement) {
		if (pEtat == null) {
			throw new NullPointerException("Missing game state");
		}
		if (pMouvement == null) {
			throw new NullPointerException("Missing move");
		}

		final boolean trait = pEtat.isWhiteActive();
		final Piece piece = pMouvement.getPiece();
		final PieceType t = piece.getType();
		final StringBuilder sb = new StringBuilder();
		final Square src = pMouvement.getFrom();
		final Square dst = pMouvement.getTo();
		final MoveGenerator apres = pEtat.derive(pMouvement, false);
		final int nbMvts = apres.getValidMoves(!trait).length;

		final int xSrc = src.getFile();
		final int xDst = dst.getFile();
		if ((t == KING) && (Math.abs(xSrc - xDst) > 1)) {
			// Roques...
			sb.append("0-0");
			if (xSrc > xDst) {
				sb.append("-0");
			}
		} else {
			// Normal...
			sb.append(t.getSANLetter());

			// Recherche et levée des éventuelles ambiguités...
			if (t != PAWN) {
				final List<Move> mvts = new ArrayList<Move>(Arrays.asList(pEtat
						.getValidMoves(trait)));
				for (int i = mvts.size() - 1; i >= 0; i--) {
					final Move m = mvts.get(i);
					if ((piece != m.getPiece()) || (dst != m.getTo())
							|| (m.equals(pMouvement))) {
						mvts.remove(i);
					}
				}
				boolean preciser = true;
				for (int i = mvts.size() - 1; i >= 0; i--) {
					final Move m = mvts.get(i);
					if (xSrc != m.getFrom().getFile()) {
						mvts.remove(i);
						if (preciser) {
							sb.append((char) ('a' + xSrc));
							preciser = false;
						}
					}
				}
				final int ySrc = src.getRank();
				for (int i = mvts.size() - 1; i >= 0; i--) {
					final Move m = mvts.get(i);
					if (ySrc != m.getFrom().getRank()) {
						sb.append((char) ('1' + ySrc));
						break;
					}
				}
			}

			if ((pEtat.getPieceAt(dst) != null)
					|| ((dst == pEtat.getEnPassant()) && (t == PAWN))) {
				// Prise...
				if (t == PAWN) {
					sb.append((char) ('a' + xSrc));
				}
				sb.append('x');
			}

			sb.append(dst.getFENString());

			if (t == PAWN) {
				// Cas particuliers...
				if (dst == pEtat.getEnPassant()) {
					// ... de la prise en passant...
					sb.append(" e.p.");
				} else {
					// ... ou de la promotion...
					final int yDst = dst.getRank();
					if ((trait && (yDst == RANK_COUNT - 1))
							|| ((!trait) && (yDst == 0))) {
						// Le '=' n'est pas dans la version de SAN de la FIDE :
						// sb.append('=');
						final Piece promotion = pMouvement.getPromotion();
						if (promotion == null) {
							assert false;
							sb.append(QUEEN.getSANLetter());
						} else {
							final PieceType promotionType = promotion.getType();
							if (promotionType != PAWN) {
								sb.append(promotionType.getSANLetter());
							} else {
								assert false;
							}
						}
					}
				}
			}
		}

		if (apres.isInCheck(!trait)) {
			// Echec / Mat ...
			sb.append('+');
			if (nbMvts == 0) {
				sb.append('+');
			}
		} else if (nbMvts == 0) {
			// Pat ...
			sb.append("(=)");
		}

		final String res = sb.toString();
		assert SAN_VALIDATOR.matcher(res).matches();
		return res;
	}
}
